Skip to content

feat(platform): add Windows support, migrate to new Zenable CLI#90

Merged
JonZeolla merged 51 commits intomainfrom
update
Apr 5, 2026
Merged

feat(platform): add Windows support, migrate to new Zenable CLI#90
JonZeolla merged 51 commits intomainfrom
update

Conversation

@JonZeolla
Copy link
Copy Markdown
Member

@JonZeolla JonZeolla commented Apr 3, 2026

Summary

  • Zenable CLI migration: Replace zenable-mcp Python package with compiled CLI binary from cli.zenable.app, auto-installed during cookiecutter project generation with checksum verification
  • Windows support: Full Windows CI support for generated projects — template generation, init, unit tests, Docker build (via WSL 2), Docker image verification, and zenable CLI verification
  • Key fixes:
    • Fix PowerShell installer to use temp file instead of stdin piping (stdin silently fails)
    • Fix zip extraction to only rename the top-level template dir (was renaming {{cookiecutter.project_slug}} dirs too)
    • Fix Taskfile var ordering so VERSION resolves after RUN_SCRIPT/SCRIPTS_DIR
    • Use bash for LOCAL_PLATFORM shell script (Windows needs explicit bash)
    • Make init-docker-multiplatform skip gracefully when buildx unavailable
    • Add diagnostic logging to post-gen hook for zenable install failures

Test plan

  • Windows CI: project generation, init, unit tests all pass
  • Windows CI: Docker image builds via WSL 2 + Docker Engine
  • Windows CI: Docker image runs and imports project module successfully
  • Windows CI: zenable version confirms CLI is installed and functional
  • Linux CI: lint, test (unit + integration), SBOM, license-check, vulnscan all pass
  • Full pipeline finalizer passes

🤖 Generated with Claude Code

Replace the Python-based zenable-mcp package (installed via uvx) with
the compiled Zenable CLI binary from cli.zenable.app. The CLI is now
auto-installed during cookiecutter generation using the official
install script, with curl/wget fallback and non-interactive mode.

Also fix trufflehog pre-commit hook to work in git worktrees by
resolving the repo root via git-common-dir.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@ai-coding-guardrails ai-coding-guardrails bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got 2 comments for you to consider

Reviewed with 🤟 by Zenable

JonZeolla and others added 19 commits April 3, 2026 13:14
Guard against detached HEAD by falling back to commit SHA. Use HEAD~1
instead of main~1 to avoid dependency on main branch existing locally.
Install trufflehog binary in CI bootstrap since language: system
requires it in PATH.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add docstring note that cli.zenable.app/install.sh performs cosign
signature verification and checksum validation of the downloaded binary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add conftest, docstrings, and taskfile to both the root and template
dictionaries to fix cspell lint failures in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a windows-latest job that generates a project from the template
and verifies it renders correctly with all defaults. The bootstrap
action is updated to handle Windows by skipping Unix-specific steps
(Homebrew, trufflehog, sha256sum) and using PowerShell alternatives.

The smoke test verifies:
- Project directory is created
- Key files exist (pyproject.toml, Taskfile.yml, Dockerfile, etc.)
- No unrendered cookiecutter variables remain
- Git repo is initialized with at least one commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The template directory name contains pipe and quote characters
({{cookiecutter.project_name|replace(" ", "")}}) which are invalid
on NTFS. Set core.protectNTFS=false before checkout to allow these
paths in the Windows smoke test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
actions/checkout overrides HOME with a temp directory, so global git
config set in a prior step is lost. Use system-level config instead
which persists across HOME changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
actions/checkout overrides both HOME and system config. Use the
GIT_CONFIG_GLOBAL environment variable to point at a custom gitconfig
file that disables core.protectNTFS, which persists through all of
checkout's internal git operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Actionlint flags github.head_ref as potentially untrusted in inline
scripts. Pass it through an environment variable instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cookiecutter internally runs git clone, which also fails on NTFS due
to the template directory name containing quote characters. Use
GIT_CONFIG_COUNT/KEY/VALUE environment variables to inject
core.protectNTFS=false into all git invocations without needing a
config file or HOME directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Double-quote characters in the template directory name are
fundamentally illegal on NTFS — no git config can work around this.
Use cookiecutter's zip URL support instead, which extracts via
Python's zipfile module and avoids NTFS filesystem restrictions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cookiecutter's zip URL support still fails because Python's zipfile
extraction on Windows cannot create paths with double-quote characters.
Download the zip manually, extract with Python (which can handle the
entries internally), then point cookiecutter at the extracted directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Python's zipfile.extractall() strips double-quote characters from
paths on Windows since NTFS rejects them. Use the \\?\ extended-length
path prefix when extracting, which bypasses NTFS filename validation
and allows the template directory with quotes to be created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The \\?\ extended-length path prefix requires backslashes and no
trailing slashes. Zip entries use forward slashes which must be
converted to OS-native separators before joining with the prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Double quotes are fundamentally illegal in NTFS paths — even the \\?\
extended-length prefix cannot work around this. Replace " with ' during
zip extraction, which is safe because Jinja2 treats both quote types
identically: replace(" ", "") and replace(' ', '') produce the same
output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use Git Bash (available on all Windows runners) for the zip download
and extraction. Git Bash's unzip may handle NTFS-illegal characters
differently than Python's os.makedirs through MSYS2's path translation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tter

The template directory contains | and " which are illegal on NTFS.
Extract the zip with unzip (which handles these in MSYS2), then rename
the directory to {{cookiecutter.project_name}} which is NTFS-safe and
produces identical output for the default project name (no spaces).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
unzip silently skips entries with NTFS-illegal chars. Use Python's
zipfile to extract all entries, renaming any directory containing
{{cookiecutter. to {{cookiecutter.project_name}} which is NTFS-safe
and renders identically for the default project name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fetch release metadata from cli.zenable.app/zenable/latest and verify
the install.sh SHA-256 checksum before piping it to bash. Uses Python
stdlib (urllib.request, hashlib) instead of curl/wget for downloading.
The install script itself also performs cosign signature verification
of the binary it downloads, providing defense in depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JonZeolla and others added 5 commits April 4, 2026 17:13
Introduce _https_urlopen() that validates the URL uses HTTPS before
calling urlopen, centralizing the SSRF mitigation. The noqa: S310 is
now confined to a single validated call site instead of scattered
across multiple callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use sys.platform to select the appropriate installer (install.ps1 on
Windows, install.sh elsewhere) and binary name (zenable.exe on Windows).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The installer updates PATH for future shells (via GITHUB_PATH in CI or
shell profile locally) but not the current Python process. After a
successful install, prepend ~/.zenable/bin to the process PATH so
_find_zenable_binary() can locate the binary immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace urlopen (which accepts arbitrary URL schemes) with an opener
built from HTTPSHandler only. This makes HTTPS-only enforcement
structural rather than relying on runtime validation + linter
suppression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add end-to-end verification steps to the Windows CI job: initialize the
generated project, run tests, build the Docker image, verify the image
runs, and confirm the zenable CLI is functional. Also add diagnostic
logging to the post-gen hook when the zenable binary cannot be found
after installation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JonZeolla JonZeolla changed the title feat(zenable): migrate from zenable-mcp to compiled CLI feat: migrate to compiled Zenable CLI and add Windows CI support Apr 4, 2026
Task eagerly evaluates top-level vars including VERSION which runs
`uv run python -c` to import the project. Bootstrap the venv with
`uv sync` and set PYTHONPATH so the namespace package import works
before any task commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JonZeolla and others added 9 commits April 4, 2026 19:28
Replace the namespace-package import (`from src.X import __version__`)
with a helper script that reads __init__.py directly. The script
resolves the file path relative to its own location, avoiding CWD and
import-mechanism issues on Windows. Includes ZENABLE_LOGLEVEL=DEBUG
support for diagnostics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task resolves vars in definition order, so VERSION must be defined
after the variables it references (RUN_SCRIPT, SCRIPTS_DIR).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove get_version.py in favor of inline sys.path.insert approach
(which should now work since var ordering is fixed). Disable lint/test
jobs temporarily to iterate faster on Windows support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Print CWD and src directory existence to stderr to diagnose why the
import fails on Windows. Use os.path.join for absolute path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The zip extraction was replacing ALL {{cookiecutter.*}} directory names
with {{cookiecutter.project_name}}, including nested ones like
{{cookiecutter.project_slug}}. This caused src/replace_me/ to become
src/replace-me/ on Windows, breaking Python imports. Only rename the
top-level template dir (which has NTFS-illegal pipe and quote chars).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add docker/setup-buildx-action to the Windows CI job so docker buildx
is available. Also make init-docker-multiplatform skip gracefully if
buildx is not available rather than failing the init task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BuildKit has no Windows image, so Linux Docker builds cannot work on
Windows runners. Remove Docker buildx setup, build, and image
verification steps. Docker builds are already tested by the Linux CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integration tests build and run Docker images which require Linux
containers, unavailable on Windows runners. Run only unit tests on
Windows; integration tests are covered by the Linux CI job.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JonZeolla and others added 13 commits April 4, 2026 20:46
PowerShell's -Command - does not reliably read scripts from stdin,
causing the zenable CLI installer to exit 0 without actually installing.
Write the install.ps1 to a temp file and use -File instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows smoke test is passing. Re-enable lint and test jobs, remove
ZENABLE_LOGLEVEL=DEBUG and diagnostic ls output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install Ubuntu 24.04 in WSL, set up Docker Engine inside it, and use
it to build and verify the Linux Docker image on the Windows runner.
No third-party actions required — uses native wsl commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Git Bash strips backslashes when passing paths to wsl commands.
Use PowerShell to convert RUNNER_TEMP to WSL /mnt/ path format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert the graceful buildx skip — missing buildx is a real failure.
Move WSL+Docker setup before task init and create a docker wrapper
script that routes all docker commands through WSL, so task init and
task build work transparently. Also use task build instead of manual
docker commands. Fix get_epoch.sh to use bash directly (uv run can't
execute .sh files on Windows).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task's Go-based shell (mvdan/sh) uses os/exec.LookPath which only
finds files with Windows-recognized extensions (.exe, .bat, .cmd).
Replace the bash wrapper with a .bat file that routes docker commands
through WSL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The .bat wrapper is found by Task's mvdan/sh but not by Git Bash.
Add a bash script wrapper too so both Task and direct bash steps
route docker commands through WSL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_zip.py

Move the inline Python zip extraction into a proper script file that
can be tested and maintained independently. The CI downloads it from
the raw GitHub URL since the repo cannot be checked out on Windows.
Also add python3 --version check to Docker image verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add argparse to main.py so the generated project supports --version
(from __init__.__version__) and --help out of the box. Update Docker
verify to test both flags directly via the container entrypoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mock sys.argv in test_main_function to avoid pytest arg conflicts.
Add test_main_version and test_main_as_script_version to verify
--version works both in-process and as a subprocess.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import __version__ from the package instead of hardcoding "0.0.0" so
the test doesn't break after a release increments the version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JonZeolla JonZeolla enabled auto-merge (squash) April 5, 2026 11:39
@JonZeolla JonZeolla disabled auto-merge April 5, 2026 11:39
@JonZeolla JonZeolla changed the title feat(template): migrate to compiled Zenable CLI and add Windows support feat: add Windows support, migrate to com Zenable CLI Apr 5, 2026
@JonZeolla JonZeolla changed the title feat: add Windows support, migrate to com Zenable CLI feat: add Windows support, migrate to new Zenable CLI Apr 5, 2026
@JonZeolla JonZeolla changed the title feat: add Windows support, migrate to new Zenable CLI feat(platform): add Windows support, migrate to new Zenable CLI Apr 5, 2026
@JonZeolla JonZeolla merged commit bf8ebc8 into main Apr 5, 2026
7 of 9 checks passed
@JonZeolla JonZeolla deleted the update branch April 5, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant